﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Castle.DynamicProxy;
using PropertyExpression.Common;
using PropertyExpression.ControllableQuery.TypedReader;

namespace PropertyExpression.ControllableQuery.Test
{
    public static class DataReaderExtensions
    {
        public static TProperty GetValue<T, TProperty>(this IDataReader<T> it, Expression<Func<T, TProperty>> propertyExpression, Func<object, TProperty> func)
        {
            return func(it.SqlDataReader[((MemberExpression)propertyExpression.Body).Member.Name]);
        }

        public static Guid GetValue<T>(this IDataReader<T> it, Expression<Func<T, Guid>> propertyExpression)
        {
            return it.GetValue(propertyExpression, GuidConverter);
        }

        private static Func<object, Guid> GuidConverter
        {
            get { return value => (Guid)value; }
        }

        public static int GetValue<T>(this IDataReader<T> it, Expression<Func<T, int>> propertyExpression)
        {
            return it.GetValue(propertyExpression, IntConverter);
        }

        private static Func<object, int> IntConverter
        {
            get { return value => (int)value; }
        }

        public static Decimal<TPrecision, TScale> GetValue<T, TPrecision, TScale>(
            this IDataReader<T> it, Expression<Func<T, Decimal<TPrecision, TScale>>> propertyExpression)
            where TPrecision : IPrecision where TScale : IScale
        {
            return it.GetValue(propertyExpression, value => new Decimal<TPrecision, TScale>((decimal) value));
        }

        public static Money GetValue<T>(this IDataReader<T> it, Expression<Func<T, Money>> propertyExpression)
        {
            return it.GetValue(propertyExpression, value => new Money((decimal)value));
        }

        public static NVarChar<TLength> GetValue<T, TLength>(this IDataReader<T> it, Expression<Func<T, NVarChar<TLength>>> propertyExpression)
            where TLength : ILength
        {
            return it.GetValue(propertyExpression, NVarCharConverter<TLength>());
        }

        private static Func<object, NVarChar<TLength>> NVarCharConverter<TLength>() where TLength : ILength
        {
            return value => new NVarChar<TLength>((string) value);
        }

        private static Func<object, string> StringConverter
        {
            get { return value => (string) value; }
        }

        private static Func<object, DateTime> DateTimeConverter
        {
            get { return value => (DateTime)value; }
        }

        private static Func<object, int?> NullableIntConverter
        {
            get
            {
                return value =>
                           {
                               if (value == DBNull.Value)
                               {
                                   return null;
                               }
                               return (int) value;
                           };
            }
        }

        public static IOption<TProperty> GetValue<T, TProperty>(this IDataReader<T> it, Expression<Func<T, IOption<TProperty>>> propertyExpression)
        {
            return it.GetValue(propertyExpression, value => value == DBNull.Value ? Option.Nothing<TProperty>() : ((TProperty)value).AsOption());
        }

        public static IOption<NVarChar<TProperty>> GetValue<T, TProperty>(this IDataReader<T> it, Expression<Func<T, IOption<NVarChar<TProperty>>>> propertyExpression)
            where TProperty : ILength
        {
            return it.GetValue(propertyExpression, value => value == DBNull.Value ? Option.Nothing<NVarChar<TProperty>>() : new NVarChar<TProperty>((string)value).AsOption());
        }

        public static T Materialize0<T>(this IDataReader<T> it)
        {
            return ProxyCreator.CreateInterfaceProxy<T>(
                new Interceptor<T>(
                    Cache<T>.PropertyInfos.ToDictionary(
                        propertyInfo => propertyInfo.Name,
                        propertyInfo => typeMap[propertyInfo.PropertyType.TypeHandle](it.SqlDataReader[propertyInfo.Name]))));
        }

        private static readonly Dictionary<RuntimeTypeHandle, Func<object, object>> typeMap;
        static DataReaderExtensions()
        {
            typeMap =
                new Dictionary<RuntimeTypeHandle, Func<object, object>>
                    {
                        {typeof (Guid).TypeHandle, _ => GuidConverter(_)},
                        {typeof (int).TypeHandle, _ => IntConverter(_)},
                        {typeof (NVarChar<L4000>).TypeHandle, _ => NVarCharConverter<L4000>()(_)},
                        {typeof (string).TypeHandle, _ => StringConverter(_)},
                        {typeof (DateTime).TypeHandle, _ => DateTimeConverter(_)},
                        {typeof (int?).TypeHandle, _ => NullableIntConverter(_)},
                    };
        }

        private static class Cache<T>
        {
            private static readonly Dictionary<string, PropertyInfo> dictionary;

            static Cache()
            {
                dictionary = PropertyInfos.Select(propertyInfo => new { GetMethod = propertyInfo.GetGetMethod(), propertyInfo })
                    .ToDictionary(_ => _.GetMethod.Name, _ => _.propertyInfo);
            }

            public static readonly PropertyInfo[] PropertyInfos = typeof(T).GetProperties();

            public static PropertyInfo GetProperty(MethodInfo methodInfo)
            {
                return dictionary[methodInfo.Name];
            }
        }

        private class Interceptor<T> : IInterceptor
        {
            private readonly Dictionary<string, object> dictionary;

            public Interceptor(Dictionary<string, object> dictionary)
            {
                this.dictionary = dictionary;
            }

            public void Intercept(IInvocation invocation)
            {
                invocation.ReturnValue = dictionary[Cache<T>.GetProperty(invocation.Method).Name];
            }
        }
    }
}